#
# workload_model.py
#
# This source file is part of the FoundationDB open source project
#
# Copyright 2013-2024 Apple Inc. and the FoundationDB project authors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#

import functools
import numpy
import math

import rate_model
from priority import Priority


@functools.total_ordering
class Request:
    def __init__(self, time, count, priority):
        self.time = time
        self.count = count
        self.priority = priority

    def __lt__(self, other):
        return self.priority < other.priority


class PriorityWorkloadModel:
    def __init__(
        self, priority, rate_model, batch_model, generator, max_outstanding=1e9
    ):
        self.priority = priority
        self.rate_model = rate_model
        self.batch_model = batch_model
        self.generator = generator
        self.max_outstanding = max_outstanding
        self.outstanding = 0

    def next_request(self, time):
        if self.outstanding >= self.max_outstanding:
            return None

        batch_size = self.batch_model.next_batch()
        self.outstanding += batch_size
        interval = self.generator.next_request_interval(self.rate_model.get_rate(time))
        return Request(time + interval, batch_size, self.priority)

    def request_completed(self, request):
        was_full = self.max_outstanding <= self.outstanding
        self.outstanding -= request.count

        return was_full and self.outstanding < self.max_outstanding


class WorkloadModel:
    def __init__(self, workload_models):
        self.workload_models = workload_models

    def priorities(self):
        return list(self.workload_models.keys())

    def next_request(self, time, priority):
        return self.workload_models[priority].next_request(time)

    def request_completed(self, request):
        return self.workload_models[request.priority].request_completed(request)


class Distribution:
    def exponential(x):
        return numpy.random.exponential(x)

    def uniform(x):
        return numpy.random.uniform(0, 2.0 * x)

    def fixed(x):
        return x


class BatchGenerator:
    def __init__(self):
        pass

    def next_batch(self):
        pass


class DistributionBatchGenerator(BatchGenerator):
    def __init__(self, distribution, size):
        BatchGenerator.__init__(self)
        self.distribution = distribution
        self.size = size

    def next_batch(self):
        return math.ceil(self.distribution(self.size))


class RequestGenerator:
    def __init__(self):
        pass

    def next_request_interval(self, rate):
        pass


class DistributionRequestGenerator(RequestGenerator):
    def __init__(self, distribution):
        RequestGenerator.__init__(self)
        self.distribution = distribution

    def next_request_interval(self, rate):
        if rate == 0:
            return 1e9

        return self.distribution(1.0 / rate)


predefined_workloads = {}

predefined_workloads["slow_exponential"] = WorkloadModel(
    {
        Priority.DEFAULT: PriorityWorkloadModel(
            Priority.DEFAULT,
            rate_model.FixedRateModel(100),
            DistributionBatchGenerator(Distribution.fixed, 1),
            DistributionRequestGenerator(Distribution.exponential),
            max_outstanding=100,
        )
    }
)

predefined_workloads["fixed_uniform"] = WorkloadModel(
    {
        Priority.SYSTEM: PriorityWorkloadModel(
            Priority.SYSTEM,
            rate_model.FixedRateModel(0),
            DistributionBatchGenerator(Distribution.fixed, 1),
            DistributionRequestGenerator(Distribution.uniform),
            max_outstanding=10,
        ),
        Priority.DEFAULT: PriorityWorkloadModel(
            Priority.DEFAULT,
            rate_model.FixedRateModel(95),
            DistributionBatchGenerator(Distribution.fixed, 10),
            DistributionRequestGenerator(Distribution.uniform),
            max_outstanding=200,
        ),
        Priority.BATCH: PriorityWorkloadModel(
            Priority.BATCH,
            rate_model.FixedRateModel(1),
            DistributionBatchGenerator(Distribution.uniform, 500),
            DistributionRequestGenerator(Distribution.uniform),
            max_outstanding=200,
        ),
    }
)

predefined_workloads["batch_starvation"] = WorkloadModel(
    {
        Priority.SYSTEM: PriorityWorkloadModel(
            Priority.SYSTEM,
            rate_model.FixedRateModel(1),
            DistributionBatchGenerator(Distribution.fixed, 1),
            DistributionRequestGenerator(Distribution.uniform),
            max_outstanding=10,
        ),
        Priority.DEFAULT: PriorityWorkloadModel(
            Priority.DEFAULT,
            rate_model.IntervalRateModel([(0, 50), (60, 150), (120, 90)]),
            DistributionBatchGenerator(Distribution.fixed, 1),
            DistributionRequestGenerator(Distribution.uniform),
            max_outstanding=200,
        ),
        Priority.BATCH: PriorityWorkloadModel(
            Priority.BATCH,
            rate_model.FixedRateModel(100),
            DistributionBatchGenerator(Distribution.fixed, 1),
            DistributionRequestGenerator(Distribution.uniform),
            max_outstanding=200,
        ),
    }
)

predefined_workloads["default_low_high_low"] = WorkloadModel(
    {
        Priority.SYSTEM: PriorityWorkloadModel(
            Priority.SYSTEM,
            rate_model.FixedRateModel(0),
            DistributionBatchGenerator(Distribution.fixed, 1),
            DistributionRequestGenerator(Distribution.uniform),
            max_outstanding=10,
        ),
        Priority.DEFAULT: PriorityWorkloadModel(
            Priority.DEFAULT,
            rate_model.IntervalRateModel([(0, 100), (60, 300), (120, 100)]),
            DistributionBatchGenerator(Distribution.fixed, 1),
            DistributionRequestGenerator(Distribution.uniform),
            max_outstanding=200,
        ),
        Priority.BATCH: PriorityWorkloadModel(
            Priority.BATCH,
            rate_model.FixedRateModel(0),
            DistributionBatchGenerator(Distribution.fixed, 1),
            DistributionRequestGenerator(Distribution.uniform),
            max_outstanding=200,
        ),
    }
)

for rate in [83, 100, 180, 190, 200]:
    predefined_workloads["default%d" % rate] = WorkloadModel(
        {
            Priority.DEFAULT: PriorityWorkloadModel(
                Priority.DEFAULT,
                rate_model.FixedRateModel(rate),
                DistributionBatchGenerator(Distribution.fixed, 1),
                DistributionRequestGenerator(Distribution.exponential),
                max_outstanding=1000,
            )
        }
    )
